Skip to content

CAMEL-23840: camel-core: pollEnrich with cacheSize(-1) does not disable consumer cache (dynamic endpoints)#24283

Open
msnijder30 wants to merge 3 commits into
apache:mainfrom
msnijder30:main
Open

CAMEL-23840: camel-core: pollEnrich with cacheSize(-1) does not disable consumer cache (dynamic endpoints)#24283
msnijder30 wants to merge 3 commits into
apache:mainfrom
msnijder30:main

Conversation

@msnijder30

Copy link
Copy Markdown
Contributor

Description

I found out about this issue with pollEnrich because we had a memory issue with the SFTP component because we did not set the cache size so it defaulted to 1000. So I read the documentation and set it to cachesize to -1, but the memory still kept going up.

Our code was using dynamic endpoints that triggered this.

pollEnrich().cacheSize(-1) is documented to disable the consumer cache entirely - polling consumers should be created fresh for each call and discarded immediately after use. However, this does not appear to work.

Root cause: PollEnricher.doBuild() unconditionally creates a DefaultConsumerCache regardless of cacheSize. DefaultConsumerCache then normalizes any cacheSize <= 0 to the CamelContext maximum cache pool size (default 1000):

// DefaultConsumerCache line 55
this.maxCacheSize = cacheSize <= 0 ? CamelContextHelper.getMaximumCachePoolSize(camelContext) : cacheSize;

So instead of discarding consumers, up to 1000 polling consumers are retained in a cache.

For resource-backed components (e.g., SFTP), each retained consumer represents an open connection that is never cleaned up until the route stops.

Reproducer

You can find my reproducer here: https://github.com/msnijder30/reproduce-camel-pollenrich-cache-issue
Also attached as zip to the JIRA ticket.

The fix

This mirrors how DefaultProducerCache (via EmptyProducerCache) already works on the producer side for cacheSize(-1).

Didn't create a DefaultConsumerCache class because it's only used in this class, unlike DefaultProducerCache which is used in multiple places.

Let me know what you think 👍 it was interesting to run into this bug

Target

  • I checked that the commit is targeting the correct branch (Camel 4 uses the main branch)

Tracking

Apache Camel coding standards and style

  • I checked that each commit in the pull request has a meaningful subject line and body.
  • I have run mvn clean install -DskipTests locally from root folder and I have committed all auto-generated changes.

AI-assisted contributions

  • If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., Co-authored-by trailers) and the PR description identifies the AI tool used.
  • Used OpenCode with DeepSeek V4 Pro

msnijder30 and others added 2 commits June 28, 2026 17:08
…ng consumer cache

The existing test only verified that the PollEnricher configured getCacheSize()
returns -1 and that dynamic endpoints are not leaked into the endpoint registry.
It did not verify that polling consumers are actually discarded after use — the
documented behavior of cacheSize(-1).

Add a test-only component whose PollingConsumer instances count doStop() calls,
proving that with cacheSize(-1) consumers remain in the DefaultConsumerCache
instead of being stopped. The test currently fails, demonstrating the bug.

Co-authored-by: OpenCode (DeepSeek V4 Pro) <contact@anoma.ly>
When cacheSize is set to -1 the consumer cache should be disabled and
polling consumers should be created fresh for each call and discarded
immediately after use. However DefaultConsumerCache normalizes any
cacheSize <= 0 to the CamelContext maximum (default 1000), causing
consumers to be retained in a bounded cache.

Fix by:
- Skipping creation of DefaultConsumerCache in doBuild() when cacheSize < 0
- Creating/stopping consumers directly in process() when consumerCache is null
- Adding null guard in getEndpointUtilizationStatistics()

This mirrors how DefaultProducerCache with cacheSize=-1 (and
EmptyProducerCache) already works for the producer side.

Co-authored-by: OpenCode (DeepSeek V4 Pro) <contact@anoma.ly>
@msnijder30 msnijder30 changed the title pollEnrich with cacheSize(-1) does not disable consumer cache (dynamic endpoints) CAMEL-23840: pollEnrich with cacheSize(-1) does not disable consumer cache (dynamic endpoints) Jun 28, 2026
@msnijder30 msnijder30 changed the title CAMEL-23840: pollEnrich with cacheSize(-1) does not disable consumer cache (dynamic endpoints) CAMEL-23840: camel-core: pollEnrich with cacheSize(-1) does not disable consumer cache (dynamic endpoints) Jun 28, 2026
@github-actions github-actions Bot added the core label Jun 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • core/camel-core-processor
  • core/camel-core
  • core/camel-support

ℹ️ Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the test-dependents label to force testing all dependents.

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • core/camel-core: 2 test(s) disabled on GitHub Actions
Build reactor — dependencies compiled but only changed modules were tested (3 modules)
  • Camel :: Core
  • Camel :: Core Processor
  • Camel :: Support

⚙️ View full build and test results

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix and the clear root-cause analysis — the bug is real and the test is well done.

However, the approach of setting consumerCache = null and scattering if (consumerCache != null) checks diverges from the established pattern on the producer side. SendDynamicProcessor, RecipientList, and RoutingSlip all use EmptyProducerCache (extends DefaultProducerCache) when cacheSize < 0 — it overrides acquireProducer() to always create new, and releaseProducer() to always stop/shutdown. No null checks needed in the calling code.

Please create an EmptyConsumerCache (extends DefaultConsumerCache) in core/camel-support/src/main/java/org/apache/camel/support/cache/ that mirrors EmptyProducerCache, and use it in PollEnricher.doBuild() so that consumerCache is never null. This keeps the codebase consistent and avoids the null checks.

The test additions are great — please keep those.

This review does not replace specialized AI review tools or static analysis.

This review was generated by an AI agent (Claude Code on behalf of Claus Ibsen) and may contain inaccuracies. Please verify all suggestions before applying.

…e pattern

The previous fix for pollEnrich cacheSize=-1 skipped creation of the
consumer cache and scattered `if (consumerCache != null)` guards through
PollEnricher. This diverges from the established producer-side pattern,
where SendDynamicProcessor / RecipientList / RoutingSlip use
EmptyProducerCache (extends DefaultProducerCache) so the cache reference
is never null and the acquire/release contract stays uniform.

Address the peer-review feedback by mirroring the producer side:

- Add EmptyConsumerCache (extends DefaultConsumerCache) that overrides
  acquirePollingConsumer() to always create+start a fresh consumer and
  releasePollingConsumer() to stopAndShutdown it, and returns 0 from
  getCapacity(). Equivalent to EmptyProducerCache.
- Update DefaultConsumerCache to conditionally create its service pool
  only when cacheSize >= 0 (mirroring DefaultProducerCache), and null-
  guard size()/purge()/cleanUp() so the EmptyConsumerCache base instance
  does not touch the pool.
- Rewrite PollEnricher.doBuild() to pick EmptyConsumerCache when
  cacheSize < 0 and DefaultConsumerCache otherwise, matching
  SendDynamicProcessor.doBuild(). Remove the now-redundant null guards
  in getEndpointUtilizationStatistics() and the acquire/release sites;
  they collapse back to unconditional consumerCache.* calls.
- Add EmptyConsumerCacheTest mirroring EmptyProducerCacheTest, asserting
  size() stays 0 across 1000+ acquire/release cycles.

Co-authored-by: OpenCode (GLM 5.2) <contact@anoma.ly>
@msnijder30 msnijder30 requested a review from davsclaus June 28, 2026 19:25

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update — this now correctly mirrors the EmptyProducerCache / DefaultProducerCache pattern. The DefaultConsumerCache changes match DefaultProducerCache's null-pool handling, EmptyConsumerCache mirrors EmptyProducerCache, and PollEnricher.doBuild() follows SendDynamicProcessor.doBuild(). Clean, consistent, and well tested.

This review was generated by an AI agent (Claude Code on behalf of Claus Ibsen) and may contain inaccuracies. Please verify all suggestions before applying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants